function project3D(x, y, z, vars) {
  x -= vars.camX;

  y -= vars.camY - 8;

  z -= vars.camZ;

  let p = Math.atan2(x, z);

  let d = Math.hypot(x, z);

  x = Math.sin(p - vars.yaw) * d;

  z = Math.cos(p - vars.yaw) * d;

  p = Math.atan2(y, z);

  d = Math.hypot(y, z);

  y = Math.sin(p - vars.pitch) * d;

  z = Math.cos(p - vars.pitch) * d;

  const rx1 = -1000;

  const ry1 = 1;

  const rx2 = 1000;

  const ry2 = 1;

  const rx3 = 0;

  const ry3 = 0;

  const rx4 = x;

  const ry4 = z;

  const uc = (ry4 - ry3) * (rx2 - rx1) - (rx4 - rx3) * (ry2 - ry1);

  const ua = ((rx4 - rx3) * (ry1 - ry3) - (ry4 - ry3) * (rx1 - rx3)) / uc;

  const ub = ((rx2 - rx1) * (ry1 - ry3) - (ry2 - ry1) * (rx1 - rx3)) / uc;

  if (!z) z = 0.000_000_001;

  return ua > 0 && ua < 1 && ub > 0 && ub < 1
    ? {
        x: vars.cx + (rx1 + ua * (rx2 - rx1)) * vars.scale,

        y: vars.cy + (y / z) * vars.scale,

        d: x * x + y * y + z * z,
      }
    : { d: -1 };
}

function elevation(x, y, z) {
  const dist = Math.hypot(x, y, z);

  if (dist && z / dist >= -1 && z / dist <= 1) return Math.acos(z / dist);

  return 0.000_000_01;
}

function rgb(col) {
  col += 0.000_001;

  const r = Number.parseInt((0.5 + Math.sin(col) * 0.5) * 16);

  const g = Number.parseInt((0.5 + Math.cos(col) * 0.5) * 16);

  const b = Number.parseInt((0.5 - Math.sin(col) * 0.5) * 16);

  return `#${r.toString(16)}${g.toString(16)}${b.toString(16)}`;
}

function interpolateColors(RGB1, RGB2, degree) {
  const w2 = degree;

  const w1 = 1 - w2;

  return [
    w1 * RGB1[0] + w2 * RGB2[0],

    w1 * RGB1[1] + w2 * RGB2[1],

    w1 * RGB1[2] + w2 * RGB2[2],
  ];
}

function rgbArray(col) {
  col += 0.000_001;

  const r = Number.parseInt((0.5 + Math.sin(col) * 0.5) * 256);

  const g = Number.parseInt((0.5 + Math.cos(col) * 0.5) * 256);

  const b = Number.parseInt((0.5 - Math.sin(col) * 0.5) * 256);

  return [r, g, b];
}

function colorString(arr) {
  const r = Number.parseInt(arr[0]);

  const g = Number.parseInt(arr[1]);

  const b = Number.parseInt(arr[2]);

  return `#${`0${r.toString(16)}`.slice(-2)}${`0${g.toString(16)}`.slice(
    -2,
  )}${`0${b.toString(16)}`.slice(-2)}`;
}

function process(vars) {
  if (vars.points.length < vars.initParticles)
    for (let i = 0; i < 5; ++i) spawnParticle(vars);

  const p = Math.atan2(vars.camX, vars.camZ);

  let d = Math.hypot(vars.camX, vars.camZ);

  d -= Math.sin(vars.frameNo / 80) / 25;

  const t = Math.cos(vars.frameNo / 300) / 165;

  vars.camX = Math.sin(p + t) * d;

  vars.camZ = Math.cos(p + t) * d;

  vars.camY = -Math.sin(vars.frameNo / 220) * 15;

  vars.yaw = Math.PI + p + t;

  vars.pitch = elevation(vars.camX, vars.camZ, vars.camY) - Math.PI / 2;

  // let t;
  // console.log(vars);
  for (let i = 0; i < vars.points.length; ++i) {
    // if (!vars.points[i]) return;
    // console.log('vars.points[i', vars.points[i].x);
    const x = vars.points[i].x;

    // const y = vars.points[i].y;

    const z = vars.points[i].z;

    const d = Math.hypot(x, z) / 1.0075;

    const t = 0.1 / (1 + (d * d) / 5);

    const p = Math.atan2(x, z) + t;

    vars.points[i].x = Math.sin(p) * d;

    vars.points[i].z = Math.cos(p) * d;

    vars.points[i].y +=
      vars.points[i].vy * t * ((Math.sqrt(vars.distributionRadius) - d) * 2);

    if (vars.points[i].y > vars.vortexHeight / 2 || d < 0.25) {
      vars.points.splice(i, 1);

      spawnParticle(vars);
    }
  }
}

function drawFloor(vars) {
  let a, d, point, x, y, z;

  for (let i = -25; i <= 25; i += 1) {
    for (let j = -25; j <= 25; j += 1) {
      x = i * 2;

      z = j * 2;

      y = vars.floor;

      const d = Math.hypot(x, z);

      point = project3D(x, y - (d * d) / 85, z, vars);

      if (point.d !== -1) {
        const size = 1 + 15_000 / (1 + point.d);

        a = 0.15 - (d / 50) ** 4 * 0.15;

        if (a > 0) {
          vars.ctx.fillStyle = colorString(
            interpolateColors(
              rgbArray(d / 26 - vars.frameNo / 40),

              [0, 128, 32],

              0.5 + Math.sin(d / 6 - vars.frameNo / 8) / 2,
            ),
          );

          vars.ctx.globalAlpha = a;

          vars.ctx.fillRect(
            point.x - size / 2,

            point.y - size / 2,

            size,

            size,
          );
        }
      }
    }
  }

  vars.ctx.fillStyle = '#82f';

  for (let i = -25; i <= 25; i += 1) {
    for (let j = -25; j <= 25; j += 1) {
      x = i * 2;

      z = j * 2;

      y = -vars.floor;

      d = Math.hypot(x, z);

      point = project3D(x, y + (d * d) / 85, z, vars);

      if (point.d !== -1) {
        const size = 1 + 15_000 / (1 + point.d);

        const a = 0.15 - (d / 50) ** 4 * 0.15;

        if (a > 0) {
          vars.ctx.fillStyle = colorString(
            interpolateColors(
              rgbArray(-d / 26 - vars.frameNo / 40),

              [32, 0, 128],

              0.5 + Math.sin(-d / 6 - vars.frameNo / 8) / 2,
            ),
          );

          vars.ctx.globalAlpha = a;

          vars.ctx.fillRect(
            point.x - size / 2,

            point.y - size / 2,

            size,

            size,
          );
        }
      }
    }
  }
}

function sortFunction(a, b) {
  return b.dist - a.dist;
}

function draw(vars) {
  vars.ctx.globalAlpha = 0.15;

  vars.ctx.fillStyle = '#000';

  vars.ctx.fillRect(0, 0, canvas.width, canvas.height);

  drawFloor(vars);

  let a, point, x, y, z;

  for (let i = 0; i < vars.points.length; ++i) {
    x = vars.points[i].x;

    y = vars.points[i].y;

    z = vars.points[i].z;

    point = project3D(x, y, z, vars);

    if (point.d !== -1) {
      vars.points[i].dist = point.d;

      const size = 1 + vars.points[i].radius / (1 + point.d);

      const d = Math.abs(vars.points[i].y);

      a = 0.8 - (d / (vars.vortexHeight / 2)) ** 1000 * 0.8;

      vars.ctx.globalAlpha = a >= 0 && a <= 1 ? a : 0;

      vars.ctx.fillStyle = rgb(vars.points[i].color);

      if (
        point.x > -1 &&
        point.x < vars.canvas.width &&
        point.y > -1 &&
        point.y < vars.canvas.height
      )
        vars.ctx.fillRect(
          point.x - size / 2,

          point.y - size / 2,

          size,

          size,
        );
    }
  }

  vars.points.sort(sortFunction);
}

function spawnParticle(vars) {
  //  ls, p;

  const pt = {};

  const p = Math.PI * 2 * Math.random();

  const ls = Math.sqrt(Math.random() * vars.distributionRadius);

  pt.x = Math.sin(p) * ls;

  pt.y = -vars.vortexHeight / 2;

  pt.vy = vars.initV / 20 + Math.random() * vars.initV;

  pt.z = Math.cos(p) * ls;

  pt.radius = 200 + 800 * Math.random();

  pt.color = pt.radius / 1000 + vars.frameNo / 250;

  vars.points.push(pt);
}

export function frame(vars) {
  if (vars === undefined) {
    vars = {};
    vars.canvas = document.querySelector('canvas');

    vars.ctx = vars.canvas.getContext('2d');

    vars.canvas.width = document.body.clientWidth;

    vars.canvas.height = document.body.clientHeight;

    window.addEventListener(
      'resize',

      () => {
        vars.canvas.width = document.body.clientWidth;

        vars.canvas.height = document.body.clientHeight;

        vars.cx = vars.canvas.width / 2;

        vars.cy = vars.canvas.height / 2;
      },

      true,
    );

    vars.frameNo = 0;

    vars.camX = 0;

    vars.camY = 0;

    vars.camZ = -14;

    vars.pitch = elevation(vars.camX, vars.camZ, vars.camY) - Math.PI / 2;

    vars.yaw = 0;

    vars.cx = vars.canvas.width / 2;

    vars.cy = vars.canvas.height / 2;

    vars.bounding = 10;

    vars.scale = 500;

    vars.floor = 26.5;

    vars.points = [];

    vars.initParticles = 700;

    vars.initV = 0.01;

    vars.distributionRadius = 800;

    vars.vortexHeight = 25;
  }

  vars.frameNo++;

  requestAnimationFrame(() => {
    frame(vars);
  });

  process(vars);

  draw(vars);
}
